#!/usr/bin/env python

# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007 by Infiscape Corporation
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

# This is a relatively advanced test program for PyOpenSG clustering. This one
# is different from ClusterClient.py insofar as it displays the scene in the
# client-side navigation window and it supports assymetric viewing volumes for
# server-side rendering.
#
# To test it, run 
#   ./ClusterServer.py -geometry 300x300+200+100 -m -w test1 &
#   ./ClusterServer.py -geometry 300x300+500+100 -m -w test2 &
#   ./ClusterClientAdv.py -m -f Data/tie.wrl test1 test2
#
# If you have trouble with multicasting, you can alternatively try
#   ./ClusterServer.py -geometry 300x300+200+100 -w 127.0.0.1:30000 &
#   ./ClusterServer.py -geometry 300x300+500+100 -w 127.0.0.1:30001 &
#   ./ClusterClientAdv.py -m -f Data/tie.wrl 127.0.0.1:30000 127.0.0.1:30001
# 
# The client will open an emoty window that you can use to navigate. The
# display is shown in the server windows.
#
# This will run all three on the same machine, but you can also start the 
# servers anywhere else, as long as you can reach them via multicast.

import math
import traceback

import osg2 as osg
from OpenGL.GL import *
from OpenGL.GLUT import *
#import gmtl


# Global scene manager reference.
gMgr = None
gClientWindow = None
gCommunicator = None
gNavCore = None
gMoveWorld = False

# ============================================================================
# Classes.
# ============================================================================

class Projector:
   def __init__(self, thetaLeft, thetaRight, thetaBottom, thetaTop, heading,
                pitch, roll, near, far):
      self.left   = near * math.tan(math.radians(thetaLeft))
      self.right  = near * math.tan(math.radians(thetaRight))
      self.bottom = near * math.tan(math.radians(thetaBottom))
      self.top    = near * math.tan(math.radians(thetaTop))

      self.near   = near
      self.far    = far

      self.quat   = self._makeQuaternion(math.radians(heading),
                                         math.radians(pitch),
                                         math.radians(roll))

   def getRotation(self):
      return self.quat

   def getLeft(self):
      return self.left

   def getRight(self):
      return self.right

   def getBottom(self):
      return self.bottom

   def getTop(self):
      return self.top

   def getNear(self):
      return self.near

   def getFar(self):
      return self.far

   def _makeQuaternion(self, heading, pitch, roll):
      roll_over_2    = roll / 2.0
      pitch_over_2   = pitch / 2.0
      heading_over_2 = heading / 2.0

      cos_heading = math.cos(heading_over_2)
      cos_pitch   = math.cos(pitch_over_2)
      cos_roll    = math.cos(roll_over_2)
      sin_heading = math.sin(heading_over_2)
      sin_pitch   = math.sin(pitch_over_2)
      sin_roll    = math.sin(roll_over_2)

      q4 = cos_heading * cos_pitch * cos_roll + sin_heading * sin_pitch * sin_roll
      q1 = cos_heading * cos_pitch * sin_roll - sin_heading * sin_pitch * cos_roll
      q2 = cos_heading * sin_pitch * cos_roll + sin_heading * cos_pitch * sin_roll
      q3 = sin_heading * cos_pitch * cos_roll - cos_heading * sin_pitch * sin_roll

      q = osg.Quaternion()
      q.setValueAsQuat(q1, q2, q3, q4)
      return q

   def __str__(self):
      return "  Left: %(left)s\n Right: %(right)s\nBottom: %(bottom)s\n   Top: %(top)s\n  Near: %(near)s\n   Far: %(far)s" % self.__dict__

class Communicator:
   def __init__(self):
      self.connType = 'Multicast'
      self.connIf   = ''
      self.svcAddr  = ''
      self.width    = 300
      self.height   = 300
      self.hServers = -1
      self.vServers = -1
      self.servers  = []
      self.root     = None
      self.window   = None

   def setRoot(self, root):
      self.root = root

   def setConnectionType(self, type):
      self.connType = type

   def setConnectionInterface(self, interface):
      self.connIf = interface

   def setServiceAddress(self, addr):
      self.svcAddr = addr

   def setHServers(self, count):
      self.hServers = count

   def setVServers(self, count):
      self.vServers = count

   def setSize(self, width, height):
      self.width  = width
      self.height = height

   def addServer(self, serverName):
      self.servers.append(serverName)

   def render(self, renderAction):
      if self.window is not None:
         try:
            self.window.activate()
            self.window.frameInit()
            self.window.renderAllViewports(renderAction)
            self.window.swap()
            self.window.frameExit()
         except:
            pass
#            traceback.print_exc()

   def getViewports(self):
      if self.window is not None:
         return self.window.getPort()
      else:
         return []

   def connect(self, refCam):
      assert self.root is not None
      assert self.window is None

      self.window = osg.MultiDisplayWindow.create()
      self.window.setConnectionType(self.connType)
      self.window.setConnectionInterface(self.connIf)
      self.window.setServiceAddress(self.svcAddr)

      if self.hServers > -1:
         self.window.setHServers(self.hServers)
      if self.vServers > -1:
         self.window.setVServers(self.vServers)

      if len(self.servers) == 0:
         self.addServer("ClusterServer")

      self.window.setSize(self.width, self.height)
      self.window.setManageClientViewports(False)

      for s in self.servers:
         self.window.pushToServers(s)

      osg.commitChanges()

      near = refCam.getNear()
      far  = refCam.getFar()

      vp_width = 1.0 / len(self.servers)

      background = gClientWindow.getPort(0).getBackground()

      projectors = []
#      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, 0.0, -90.0,
#                                  0.0, near, far))
#      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, 0.0, 0.0,
#                                  0.0, near, far))
#      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, 0.0, 90.0,
#                                  0.0, near, far))
      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, 0.0, 27.05,
                                  0.0, near, far))
      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, -51.43, 27.05,
                                  0.0, near, far))
      projectors.append(Projector(-34.75, 34.75, -25.95, 30.37, 51.43, 27.05,
                                  0.0, near, far))

      inv_pre_proj = getInverseProjectionMatrix(refCam, gClientWindow)

      for i in range(len(self.servers)):
         if gMoveWorld:
            camera = osg.MatrixCamera.create()
         else:
            camera = osg.MatrixCameraDecorator.create()
            camera.setDecoratee(refCam)

         p = projectors[i]
#         print p

         rot_mat = osg.Matrix()
         rot_mat.setRotate(p.getRotation())

         if gMoveWorld:
            camera.setModelviewMatrix(rot_mat)
         else:
            camera.setPostViewing(rot_mat)

         m = osg.Matrix()
         osg.MatrixFrustum(m, p.getLeft(), p.getRight(), p.getBottom(),
                           p.getTop(), p.getNear(), p.getFar())

         if gMoveWorld:
            camera.setProjectionMatrix(m)
         else:
            camera.setPostProjection(m)
            camera.setPreProjection(inv_pre_proj)

         vp = osg.Viewport.create()
         vp.setCamera(camera)
         vp.setBackground(background)
         vp.setRoot(self.root)
         vp.setSize(i * vp_width, 0, (i + 1) * vp_width, 1)

         self.window.addPort(vp)

      osg.commitChanges()

      # Initialize the window.
      self.window.init()

   def disconnect(self):
      del self.window
      self.window = None
      osg.commitChanges()

# ============================================================================
# GLUT callback functions.
# ============================================================================

def display():
   if gMoveWorld:
      m = gMgr.getNavigator().getMatrix()
      m.invert()
      gNavCore.setMatrix(m)

   gMgr.redraw()
   gCommunicator.render(gMgr.getRenderTraversalAction())

#   ports = gClusterWindow.getPort()
#   for p in ports:
#      fv = osg.FrustumVolume()
#      p.getCamera().getFrustum(fv, p)
#      osg.drawVolume(fv)

   osg.Thread.getCurrentChangeList().clear()

   glutSwapBuffers()

def reshape(w, h):
   gClientWindow.resize(w, h)

   if not gMoveWorld:
      m = getInverseProjectionMatrix(gClientWindow.getPort(0).getCamera(),
                                     gClientWindow)
      ports = gCommunicator.getViewports()
      for p in ports:
         p.getCamera().setPreProjection(m)

   glutPostRedisplay()

def mouse(button, state, x, y):
   if state:
      gMgr.mouseButtonRelease(button, x, y)
   else:
      gMgr.mouseButtonPress(button, x, y)

   glutPostRedisplay()

def motion(x, y):
   gMgr.mouseMove(x, y)
   glutPostRedisplay()

def keyboard(k, x, y):
   if ord(k) == 27:
      sys.exit(0)
   else:
      background = gClientWindow.getPort(0).getBackground()
      bg_color = background.getColor()

      if k == 'R':
         bg_color[0] += 0.01
         background.setColor(bg_color)
      elif k == 'r':
         bg_color[0] -= 0.01
         background.setColor(bg_color)
      elif k == 'G':
         bg_color[1] += 0.01
         background.setColor(bg_color)
      elif k == 'g':
         bg_color[1] -= 0.01
         background.setColor(bg_color)
      elif k == 'B':
         bg_color[2] += 0.01
         background.setColor(bg_color)
      elif k == 'b':
         bg_color[2] -= 0.01
         background.setColor(bg_color)

   glutPostRedisplay()

def getInverseProjectionMatrix(camera, window):
   pre_proj = osg.Matrix()
   camera.getProjection(pre_proj, window.getWidth(), window.getWidth())
   pre_proj.invert()
   return pre_proj

# ============================================================================
# Main function
# ============================================================================

def main():
   global gMoveWorld

   scene = None

   args = sys.argv[1:]
   osg.osgInit(sys.argv)

   glutInit(sys.argv)
   glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE)

   winid = glutCreateWindow("PyOpenSG Navigator")

   glutReshapeFunc(reshape)
   glutDisplayFunc(display)
   glutMouseFunc(mouse)
   glutMotionFunc(motion)
   glutKeyboardFunc(keyboard)

   server_x = -1
   server_y = -1

   global gCommunicator
   gCommunicator = Communicator()

   while len(args) > 0:
      arg = args.pop(0)

      if arg.startswith('-'):
         if arg[1] == 'm':
            gCommunicator.setConnectionType('Multicast')
            print "Connection type set to Multicast"
         elif arg[1] == 'p':
            gCommunicator.setConnectionType('SockPipeline')
            print "Connection type set to SockPipeline"
         elif arg[1] == 's':
            gCommunicator.setConnectionType('StreamSock')
            print "Connection type set to StreamSock"
         elif arg[1] == 'i':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               gCommunicator.setConnectionInterface(opt)
         elif arg[1] == 'a':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               gCommunicator.setServiceAddress(opt)
         elif arg[1] == 'f':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               scene = osg.SceneFileHandler.the().read(opt)
         elif arg[1] == 'x':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               server_x = x
         elif arg[1] == 'y':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               server_y = y
         elif arg[1] == 'h':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               gCommunicator.setHServers(int(opt))
         elif arg[1] == 'v':
            try:
               if len(arg) > 2:
                  opt = arg[2:]
               else:
                  opt = args.pop(0)
            except:
               opt = None

            if opt:
               gCommunicator.setVServers(int(opt))
         elif arg[1] == 'W':
            gMoveWorld = True
      else:
         print arg
         gCommunicator.addServer(arg)

   if scene is None:
      scene = osg.makeTorus(0.5, 2, 16, 16)

   win_width  = 300
   win_height = 300

   if server_x > 0 and server_y > 0:
      gCommunicator.setSize(server_x, server_y)
   else:
      gCommunicator.setSize(win_width, win_height)

   osg.commitChanges()

   global gClientWindow
   gClientWindow = osg.GLUTWindow.create()
   glutReshapeWindow(win_width, win_height)
   gClientWindow.setId(winid)
   gClientWindow.init()
   gClientWindow.resize(win_width, win_height)

   global gMgr
   gMgr = osg.SimpleSceneManager()
   gMgr.setUseTraversalAction(True)

   root = osg.Node.create()
   root.setCore(osg.Group.create())

   #root.addChild(osg.makeTorus(0.5, 2, 16, 16))

   beacon = osg.Node.create()
   beacon.setCore(osg.Group.create())
   root.addChild(beacon)

   dlight = osg.Node.create()
   dlight_core = osg.DirectionalLight.create()
   dlight.setCore(dlight_core)
   dlight_core.setBeacon(beacon)
   dlight_core.setDirection(osg.Vec3f(0, 0, 1))
   dlight_core.setDiffuse(osg.Color4f(1, 1, 1, 1))
   dlight_core.setAmbient(osg.Color4f(0.2, 0.2, 0.2, 1))
   dlight_core.setSpecular(osg.Color4f(1, 1, 1, 1))

   dlight.addChild(scene)

   if gMoveWorld:
      global gNavCore
      gNavCore = osg.Transform.create()
      real_nav_node = osg.Node.create()
      real_nav_node.setCore(gNavCore)
      root.addChild(real_nav_node)
      real_nav_node.addChild(dlight)
   else:
      root.addChild(dlight)

   # Tell the scene manager what to manage.
   gMgr.setWindow(gClientWindow)
   #gMgr.setHeadlight(False)
   gMgr.setRoot(root)

   if gMoveWorld:
      nav_node = osg.Node.create()
      nav_node.setCore(osg.Transform.create())
      nav = gMgr.getNavigator()
      nav.setCameraTransformation(nav_node)
      gMgr.getHeadlight().setBeacon(nav_node)
      root.addChild(nav_node)

   gCommunicator.setRoot(root)

   osg.commitChanges()

   global gBeacon
   gBeacon = gMgr.getCamera().getBeacon()

   plane_material = osg.SimpleMaterial.create()
   plane_material.setAmbient(osg.Color3f(0.2, 0.2, 0.2))
   plane_material.setDiffuse(osg.Color3f(0.8, 0.5, 0.2))
   plane_material.setEmission(osg.Color3f(0.0, 0.0, 0.0))
   plane_material.setSpecular(osg.Color3f(1, 1, 1))
   plane_material.setShininess(10)
   plane_material.setTransparency(0.4)
   plane_material.setLit(True)

#   dome_radius = 55.5 / 2.0

#   ports = gClusterWindow.getPort()
#   for p in ports:
#      fv = osg.FrustumVolume()
#      p.getCamera().getFrustum(fv, p)
#      print fv
#      print fv.getLeft()

   # Show the whole scene.
   gMgr.showAll()

   osg.commitChanges()

   gCommunicator.connect(gMgr.getCamera())

   glutMainLoop()

   sys.exit(0)

if __name__ == '__main__':
   main()
